<?php
/**
 * Geneone: Content Management Framework
 * Base Object
 *
 * http://geneone.neondragon.net/
 *
 * @package Geneone
 * @author Khlo
 * @version 1.0
 * @copyright 2004-2006 Geneone Development Team
 */
 
/* All your objects inherit from us */
require_once 'Gene/Object.php';

class Gene_Object_Model {
	/**
	 * Object information.
	 *
	 * @var array
	 */
	var $info = array();
	
	/**
	 * Object ID
	 *
	 * @var integer
	 */
	var $objectID;
	
	/**
	 * Object children
	 *
	 * @var array
	 */
	var $childs = array();
	
	/**
	 * Object parents
	 *
	 * @var array
	 */
	var $parents = array();
	
	/**
	 * Default Properties
	 *
	 * @var array
	 */
	var $defaultProperties = array();
	
	/**
	 * Default User Properties
	 *
	 * @var array
	 */
	var $defaultUserProperties = array();
	
	/**
	 * Object properties.
	 *
	 * @var array
	 */
	var $properties = null;
	
	/**
	 * Object user properties.
	 *
	 * @var array
	 */
	var $userProperties = null;
	
	/**
	 * Number of childs to return using getChilds()
	 * If 0, returns them all.
	 *
	 * @var integer
	 */
	var $perPage = 0;
	
	/**
	 * Page number.
	 *
	 * @var integer
	 */
	var $pageNum;
	
	/**
	 * Permissions cache.
	 *
	 * @var array
	 */
	var $userPermissions = array();
	
	/**
	 * Create a new object.
	 *
	 * @static
	 *
	 * @var string $type Object Type
	 * @var string $title Object Title
	 * @var string $desc Object Description
	 * @var string $content Object Contents
	 * @var object $parentobj Instance of parent object
	 * @var array $options New Object options
	 *
	 * @return mixed Returns the object ID of the newly created object
	 *               on success or a PEAR_Error object containing a
	 *               descriptive error message on failure.
	 */
	function create($type, $title='', $desc='', $content='', &$parentobj, $options=array()) {
		require_once ("Gene/Object/Model/".ucfirst($type).".php");
		$obj_class = "Gene_Object_Model_".ucfirst($type);
		
		// Check parent type is allowed
		$allowedParentGuids = call_user_func(array($obj_class, "getAllowedParents"));
		$parentGuids = $parentobj->getCompatibleObjectGuids();
		$parentGuids[] = $parentobj->getObjectTypeGuid();
		
		$allowed = false;

		if (count($allowedParentGuids)) {
			foreach ($allowedParentGuids as $i) {
				if ($i == "*" OR in_array($i, $parentGuids)) {
					$allowed = true;
				}
			}
		}

		if ($allowed) {
			// Ensure child type is allowed
			$allowedChilds = $parentobj->getAllowedChilds();
			$childGuids = call_user_func(array($obj_class, "getCompatibleObjectGuids"));
			$childGuids[] = call_user_func(array($obj_class, "getObjectTypeGuid"));
			
			if (count($allowedChilds)) {
				foreach ($allowedChilds as $i) {
					if ($i == "*" OR in_array($i, $childGuids)) {
						$user =& Gene::getUser();
						if ($user->getUserId()) {
							// like $obj_class::create but it actually works
							return call_user_func(array($obj_class, "create"), $title, $desc, $content, $parentobj, $options);
						} else {
							print_r($options);
							// Guests
							$options['guest_name'] = Gene_User::canonizePublicName($options['guest_name']);

							if (!$options['guest_name']) {
								return PEAR::raiseError("You must provide a name.");
							} else if (!$options['guest_email']) {
								return PEAR::raiseError("You must provide a valid e-mail address.");
							}
							
							$newObjectId = call_user_func(array($obj_class, "create"), $title, $desc, $content, $parentobj, $options);
							if (!PEAR::isError($newObjectId) && $newObject =& Gene_Object::get($newObjectId)) {
								$db =& Gene::getDatabase();
								$db->extended->autoExecute(
									Gene::tableName("object_guests"),
									array(
										"object_id" => $newObjectId,
										"guest_name" => $options['guest_name'],
										"guest_email" => $options['guest_email'],
										"guest_url" => $options['guest_url'] ? $options['guest_url'] : null,
										"guest_ip" => $_SERVER['REMOTE_ADDR'],
									),
									MDB2_AUTOQUERY_INSERT
								);
								
							}

							return $newObjectId;
						}
					}
				}
			}
		}
		
		return PEAR::raiseError("You cannot add this type of object here.");
	}
	
	/**
	 * Updates the search index for this object.
	 *
	 * @var array $options Options:
	 *                     title integer Title Weight. Default: 3
	 *                     description integer Description Weight. Default: 2
	 *                     content integer Content Weight. Default: 1
	 *                     taxonomy array Taxonomy Set to include. Specify set id as key, weight as value.
	 *                     additional_terms integer Additional terms. Specify term as key, weight as value.
	 *                     Set weight values to 0 to disable search indexing of field.
	 */
	function updateSearchIndex($options=array()) {
		$db =& Gene::getDatabase();
		$info = $this->get();
		
		$defaultoptions = array(
			"title" => 3,
			"description" => 2,
			"content" => 1,
			"taxonomy" => array(),
			"additional_terms" => array(),
		);
		
		$options = array_merge($defaultoptions, $options);
		
		require_once ('Gene/Search.php');
		
		// Delete current search index for object
		$q = $db->prepare ("DELETE FROM ".Gene::tableName("object_search_index")." WHERE object_id=?");
		$q->execute(array($info['object_id']));
		
		$terms = array();
		
		// Title
		if ($options['title']) {
			$title = Gene_Search::getTerms($info['title']);
			foreach ($title as $j => $k) {
				if (isset($terms[$j])) {
					$terms[$j] += $k * $options['title'];
				} else {
					$terms[$j] = $k * $options['title'];
				}
			}
		}
		
		// Description
		if ($options['description']) {
			$description = Gene_Search::getTerms($info['description']);
			foreach ($description as $j => $k) {
				if (isset($terms[$j])) {
					$terms[$j] += $k * $options['description'];
				} else {
					$terms[$j] = $k * $options['description'];
				}
			}
		}
		
		// Content
		if ($options['content']) {
			$content = Gene_Search::getTerms(html_entity_decode(strip_tags($info['content'])));
			if (isset($terms[$j])) {
				$terms[$j] += $k * $options['content'];
			} else {
				$terms[$j] = $k * $options['content'];
			}
		}
		
		// Get all taxonomy terms for this object
		if (count($options['taxonomy'])) {
			foreach ($options['taxonomy'] as $j => $k) {
				$taxonomy = $this->getTaxonomyEntry($j);
				$labels = $taxonomy->getAll();
				foreach ($labels as $i) {
					$t = Gene_Search::getTerms($i);
					if (count($t)) {
						foreach ($t as $l => $m) {
							if (isset($terms[$l])) {
								$terms[$l] += $m * $k;
							} else {
								$terms[$l] = $m * $k;
							}
						}
					}
				}
			}
		}
		
		// Additional terms
		if (count($options['additional_terms'])) {
			foreach ($options['additional_terms'] as $j => $k) {
				if (isset($terms[$j])) {
					$terms[$j] += $k;
				} else {
					$terms[$j] = $k;
				}
			}
		}
		
		$q = $db->prepare ("INSERT INTO ".Gene::tableName("object_search_index")." VALUES (?, ?, ?)");
		foreach ($terms as $j => $k) {
			$q->execute(array($j, $info['object_id'], $k));
		}
	}
	
	/**
	 * Checks if an alias is valid.
	 *
	 * @static
	 *
	 * @var string $alias Alias
	 */
	function isValidAlias($alias) {
		if (!is_numeric($alias) && preg_match('/([a-zA-Z0-9\._-])$/', $alias)) {
			return true;
		}
		
		return false;
	}
	
	/**
	 * Sets the owner of the current object.
	 *
	 * @var string $ownerid New Owner ID
	 * @return bool success
	 */
	function setOwner($ownerid) {
		$ownerid = intval($ownerid);
		
		if (!$ownerid) {
			return false;
		}
		
		// Check user exists
		if (!Gene_User::fromUserID($ownerid)) {
			return false;
		}
		
		$info = $this->get();
		
		$db =& Gene::getDatabase();
		$db->extended->autoExecute(
			Gene::tableName("objects"),
			array("owner_id" => $ownerid),
			MDB2_AUTOQUERY_UPDATE,
			"object_id=".$db->quote((int)$info['object_id'])
		);
		return true;
	}
	
	/**
	 * Sets the alias of the current object.
	 *
	 * @var string $alias Object Alias
	 * @return bool success
	 */
	function setAlias($alias) {
		// Check Valid alias name
		if (!Gene_Object_Model::isValidAlias($alias)) {
			return false;
		}
		
		$info =& $this->get();
		// Check for other objects with this alias
		if ($o =& Gene_Object::get($alias, true)) {
			$obj = $o->get();
					
			// Current Object
			if ($obj['object_id'] == $info['object_id']) {
				return true;
			}
			
			return false;
		} else {
			$db =& Gene::getDatabase();
			$db->extended->autoExecute(
				Gene::tableName("objects"),
				array("alias" => $alias),
				MDB2_AUTOQUERY_UPDATE,
				"object_id=".$db->quote((int)$info['object_id'])
			);
			
			$info['alias'] = $alias;
			
			return true;
		}
	}
	
	/**
	 * Updates the permissions of this object.
	 *
	 * @var string $permissions Array with permission name as key and permission
	 *                          value as array value.
	 */
	function setPermissions($permissions) {
		$p = array();
		
		foreach ($permissions as $j => $k) {
			if (in_array($j, array("view", "edit", "revisions", "write", "moderate"))) {
				$p['p_'.$j] = $k;
			}
		}
		
		$db =& Gene::getDatabase();
		$info = $this->get();
		
		$db->extended->autoExecute(
			Gene::tableName("objects"),
			$p,
			MDB2_AUTOQUERY_UPDATE,
			"object_id=".(int)$info['object_id']
		);
		
		// Update cached info
		$this->userPermissions = array();
		$this->info = array_merge($this->info, $p);
	}
	
	/**
	 * Checks whether the user can perform a certain action.
	 * Use canRead(), canEdit(), etc.
	 *
	 * @access private
	 *
	 * @var string $permission Name of the permission. view, edit, revisions,
	 *                         write or moderate.
	 * @var string $user GeneUser object to check against.
	 *
	 * @return bool True or False
	 */
	function checkPermission($permission, &$user) {
		if (!in_array($permission, array("view", "edit", "revisions", "write", "moderate"))) {
			return false;
		}
		
		$userid = $user->getUserId();
		
		if (isset($this->userPermissions[$userid])) {
			if (isset($this->userPermissions[$userid][$permission])) {
				return $this->userPermissions[$userid][$permission];
			}
		}
		
		$info = $this->get();
		
		$usermasks = $user->getPermissionMasks();
		$objectmasks = explode(",", $info['p_'.$permission]);
		
		foreach ($objectmasks as $i) {
			if ($i == "o") {
				if ($info['owner_id'] == $userid && $userid > 0) {
					return true;
				}
			} else if (in_array($i, $usermasks)) {
				$this->userPermissions[$userid][$permission] = true;
				return true;
			}
		}

		$this->userPermissions[$userid][$permission] = false;
		return false;
	}
	
	/**
	 * Returns whether the user can view the object.
	 *
	 * @var string $userid Optionally provide a User ID to check against
	 *                     another user.
	 *
	 * @return bool True or False depending on whether user can view the object.
	 */
	function canView($userid=null) {
		if ($userid === null) {
			$user =& Gene::getUser();
		} else {
			$user =& Gene_User::fromUserID($userid);
		}
		
		$p = $this->checkPermission("view", $user);
		if ($p) {
			return $p;
		} else {
			// Moderators automatically get view, edit, revisions, new
			return $this->checkPermission("moderate", $user);
		}
	}
	
	/**
	 * Returns whether the user can edit the object.
	 *
	 * @var string $userid Optionally provide a User ID to check against
	 *                     another user.
	 *
	 * @return bool True or False depending on whether user can edit the object.
	 */
	function canEdit($userid=null) {
		if ($userid === null) {
			$user =& Gene::getUser();
		} else {
			$user =& Gene_User::fromUserID($userid);
		}
		
		$p = $this->checkPermission("edit", $user);
		if ($p) {
			return $p;
		} else {
			// Moderators automatically get view, edit, revisions, new
			return $this->checkPermission("moderate", $user);
		}
	}
	
	/**
	 * Returns whether the user can view revisions of the object.
	 *
	 * @var string $userid Optionally provide a User ID to check against
	 *                     another user.
	 *
	 * @return bool True or False depending on whether user can view revisions of the object.
	 */
	function canRevisions($userid=null) {
		if ($userid === null) {
			$user =& Gene::getUser();
		} else {
			$user =& Gene_User::fromUserID($userid);
		}
		
		$p = $this->checkPermission("revisions", $user);
		if ($p) {
			return $p;
		} else {
			// Moderators automatically get view, edit, revisions, new
			return $this->checkPermission("moderate", $user);
		}
	}
	
	/**
	 * Returns whether the user can write to the object (create child objects).
	 *
	 * @var string $userid Optionally provide a User ID to check against
	 *                     another user.
	 *
	 * @return bool True or False depending on whether user can write to the object.
	 */
	function canWrite($userid=null) {
		if ($userid === null) {
			$user =& Gene::getUser();
		} else {
			$user =& Gene_User::fromUserID($userid);
		}
		
		$p = $this->checkPermission("write", $user);
		if ($p) {
			return $p;
		} else {
			// Moderators automatically get view, edit, revisions, new
			return $this->checkPermission("moderate", $user);
		}
	}
	
	/**
	 * Returns whether the user can moderate the object.
	 *
	 * @var string $userid Optionally provide a User ID to check against
	 *                     another user.
	 *
	 * @return bool True or False depending on whether user can moderate the object.
	 */
	function canModerate($userid=null) {
		if ($userid === null) {
			$user =& Gene::getUser();
		} else {
			$user =& Gene_User::fromUserID($userid);
		}
		
		return $this->checkPermission("moderate", $user);
	}
	
	/**
	 * Bumps the number of child objects.
	 *
	 * @return integer New number of child objects
	 */
	function bumpChildNumber() {
		$db =& Gene::getDatabase();
		
		$info = $this->get();
		$info['numchilds']++;
		
		$q = $db->prepare ("UPDATE ".Gene::tableName("objects")." SET numchilds=numchilds+1 WHERE object_id=?");
		$q->execute(array($info['object_id']));
		
		return $info['numchilds'];
	}
	
	/**
	 * Moves this object and all child objects.
	 *
	 * Does not change taxonomies, permissions, etc.
	 *
	 * @var object $parent New Parent Object
	 *
	 * @return mixed bool true on success
	 *               PEAR_Error on failure
	 */
	function setParent(&$parent) {
		$info = $this->get();
		
		// If the object is already here, shut up
		if ($parent->objectID == $info['parent']) {
			return true;
		}
		
		// We can't move an object to a descendant of itself
		$pinfo = $parent->get();
		$pparentcsv = explode(",", $pinfo['parentcsv']);
		if (in_array($info['object_id'], $pparentcsv)) {
			return PEAR::raiseError("An object cannot be moved to a descendant of itself.");
		}
		if ($parent->objectID == $this->objectID) {
			return PEAR::raiseError("An object cannot be moved into itself.");
		}
		
		// Check parent type is allowed
		$allowedParentGuids = $this->getAllowedParents();
		$parentGuids = $parent->getCompatibleObjectGuids();
		$parentGuids[] = $parent->getObjectTypeGuid();
		
		$allowed = false;

		if (count($allowedParentGuids)) {
			foreach ($allowedParentGuids as $i) {
				if ($i == "*" OR in_array($i, $parentGuids)) {
					$allowed = true;
				}
			}
		}

		if (!$allowed) {
			return PEAR::raiseError("The object cannot be moved here.");
		}
		
		// Ensure child type is allowed
		$allowedChilds = $parent->getAllowedChilds();
		$childGuids = $this->getCompatibleObjectGuids();
		$childGuids[] = $this->getObjectTypeGuid();
		$allowed = false;
		
		if (count($allowedChilds)) {
			foreach ($allowedChilds as $i) {
				if ($i == "*" OR in_array($i, $childGuids)) {
					$allowed = true;
				}
			}
		}

		if (!$allowed) {
			return PEAR::raiseError("The object cannot be moved here.");
		}
		
		// OK to continue...
		$db =& Gene::getDatabase();
		
		// Update numchilds
		$q = $db->prepare ("UPDATE ".Gene::tableName("objects")." SET numchilds=numchilds-1 WHERE object_id=?");
		$q->execute(array($info['parent']));
		$parent->bumpChildNumber();
		
		// Update Parent Fields
		$q = $db->prepare ("UPDATE ".Gene::tableName("objects")." SET parent=?, parentcsv=? WHERE object_id=?");
		$q->execute(array($pinfo['object_id'], $pinfo['parentcsv'].",".$pinfo['object_id'], $info['object_id']));
		
		$q = $db->prepare ("SELECT parentcsv FROM ".Gene::tableName("objects")." WHERE parentcsv LIKE ? GROUP BY parentcsv");
		$res = $q->execute(array($info['parentcsv'].",".$info['object_id']."%"));
		$csvvalues = $res->fetchCol();
		
		$u = $db->prepare ("UPDATE ".Gene::tableName("objects")." SET parentcsv=? WHERE parentcsv=?");
		foreach ($csvvalues as $i) {
			$u->execute(array($pinfo['parentcsv'].",".$pinfo['object_id'].",".substr($i, strlen($info['parentcsv'].",")), $i));
		}
		
		return true;
	}
	
	/**
	 * Removes the current object and *all* childs.
	 *
	 * @return bool success
	 */
	function remove() {
		$info = $this->get();
		
		// Root object
		if ($info['parent'] == "-1") {
			return false;
		} else {
			$db =& Gene::getDatabase();
			$q = $db->prepare("SELECT object_id FROM ".Gene::tableName("objects")." WHERE parentcsv=? OR parentcsv LIKE ?");
			$res = $q->execute(array($info['parentcsv'].",".$info['object_id'], $info['parentcsv'].",".$info['object_id'].',%'));
			$delete = $res->fetchCol("object_id");

			$delete[] = $info['object_id'];
			
			$deletestr = implode(',', $delete);
						
			$db->query ("DELETE FROM ".Gene::tableName("objects")." WHERE object_id IN (".$deletestr.")");
			
			// Delete from revisions table
			$db->query ("DELETE FROM ".Gene::tableName("object_revisions")." WHERE object_id IN (".$deletestr.")");
			
			// Update parent numchilds
			$q = $db->prepare ("UPDATE ".Gene::tableName("objects")." SET numchilds=numchilds-1 WHERE object_id=?");
			$q->execute(array($info['parent']));
			
			// Remove Taxonomy & Properties & Guests
			$db->query ("DELETE FROM ".Gene::tableName("object_taxonomy")." WHERE object_id IN (".$deletestr.")");
			$db->query ("DELETE FROM ".Gene::tableName("object_user_taxonomy")." WHERE object_id IN (".$deletestr.")");
			$db->query ("DELETE FROM ".Gene::tableName("object_properties")." WHERE object_id IN (".$deletestr.")");
			$db->query ("DELETE FROM ".Gene::tableName("object_guests")." WHERE object_id IN (".$deletestr.")");
			
			return true;
		}
	}
	
	/**
	 * Returns a list of the parent objects.
	 *
	 * @return array List of parents objects.
	 */
	function getParents() {
		$db =& Gene::getDatabase();
		
		if (count($this->parents)) {
			// -1
			if ($this->parents == "-1") {
				return array();
			} else {
				return $this->parents;
			}
		} else {
			if ($this->info['parentcsv'] == '-1') {
				$this->parents = '-1';
				return array();
			} else {
				$parentslist = explode (",", $this->info['parentcsv']);
			
				$q = $db->query("SELECT o.*, u.name FROM ".Gene::tableName("objects")." o LEFT JOIN ".Gene::tableName("user_info")." u ON (o.owner_id=u.user_id) WHERE o.object_id IN (".$this->info['parentcsv'].")");
				$parentinfo = $q->fetchAll();
				$q->free();
				
				$p = array();
				
				/* Cache for further use */
				foreach ($parentinfo as $i) {
					Gene_Object::cacheObject($i['object_id'], $i);
					$p[$i['object_id']] = $i;
				}
				
				/* Return parents in order */
				$parents = array();
				foreach ($parentslist as $i) {
					$parents[] = $p[$i];
				}
				
				$this->parents = $parents;

				return $parents;
			}
		}
	}
	
	/**
	 * Gets the direct parent object.
	 *
	 * @return object Direct Parent Gene_Object Object
	 */
	function &getParent() {
		$info = $this->get();
		if ($info['parent'] != "-1") {
			$parent =& Gene_Object::get($info['parent']);
			return $parent;
		} else {
			$parent = false;
			return $parent;
		}
	}
	
	/**
	 * Set up object child paging
	 *
	 * @var int $num Childs per page (0 to show all)
	 */
	function enablePaging($num=10) {
		$num = (int)$num;
		$this->perPage = $num;
		$this->childs = array();
	}
	
	/**
	 * Set page number (first page is 1, second is 2, etc.)
	 *
	 * @var int $num Page Number
	 */
	function getNumPages() {
		$info = $this->get();
		return ceil($info['numchilds'] / $this->perPage);
	}
	
	/**
	 * Set page number (first page is 1, second is 2, etc.)
	 *
	 * @var int $num Page Number
	 */
	function setPageNumber($num=1) {
		$num = (int)$num;
		if (!$num) {
			$num = 1;
		}
		$this->pageNum = $num;
		$this->childs = array();
	}
	
	/**
	 * Returns a list of child objects.
	 *
	 * @var array $options List of options to modify the items returned or the
	 *                     order of items returned.
	 *                     * where string SQL WHERE clause
	 *                     * tables mixed Array listing of tables to join. If only
	 *                                    one, a string is accepted. Each of the
	 *                                    tables must have an object_id column.
	 *                     * order string SQL ORDER BY clause
	 *                     * recursive boolean Whether to return childs of childs, etc.
	 *
	 * @return array List of child objects.
	 */
	function getChilds($options=array()) {
		$db =& Gene::getDatabase();
		
		$k = md5(var_export($options,true));
		
		if (isset($this->childs[$k])) {
			return $this->childs[$k];
		}
		
		/* Where */
		if (isset($options['where'])) {
			$where = $options['where'];
		} else {
			$where = null;
		}
		
		/* Additional Tables */
		$additionalFieldsSql = "";
		$additionalTablesSql = "";
		
		if (isset($options['tables']) && count ($options['tables'])) {
			if (is_array($options['tables'])) {
				foreach ($options['tables'] as $i) {
					$additionalFieldsSql .= ", ".$i.".*";
					$additionalTablesSql .= " LEFT JOIN ".$i." ON (o.object_id=".$i.".object_id)";
				}
			} else {
				$additionalFieldsSql = ", ".$options['tables'].".*";
				$additionalTablesSql = "LEFT JOIN ".$options['tables']." ON (o.object_id=".$options['tables'].".object_id)";
			}
		}
		
		/* Ordering */
		if (isset($options['order'])) {
			$orderBy = $options['order'];
		} else {
			$orderBy = $this->orderBy;
		}
		
		if (!$where && count($this->childs)) {
			return $this->childs;
		} else {
			if ($where) {
				$where = "AND (".$where.")";
			} else {
				$where = "";
			}
			
			if ($this->perPage && $this->pageNum) {
				$offset = ($this->pageNum - 1) * $this->perPage;
				$db->setLimit($this->perPage, $offset);
			}
			
			if (isset($options['recursive']) && $options['recursive']) {
				$info = $this->get();
				$q = $db->prepare("SELECT o.*, u.name".$additionalFieldsSql.", o.object_id FROM ".Gene::tableName("objects")." o LEFT JOIN ".Gene::tableName("user_info")." u ON (o.owner_id=u.user_id) ".$additionalTablesSql." WHERE parentcsv LIKE ? ".$where." ORDER BY ".$orderBy);
				$res = $q->execute(array($info['parentcsv'].",".$this->objectID."%"));
			} else {
				$q = $db->prepare("SELECT o.*, u.name".$additionalFieldsSql.", o.object_id FROM ".Gene::tableName("objects")." o LEFT JOIN ".Gene::tableName("user_info")." u ON (o.owner_id=u.user_id) ".$additionalTablesSql." WHERE parent=? ".$where." ORDER BY ".$orderBy);
				$res = $q->execute(array($this->objectID));
			}

			$results = $res->fetchAll();
			
			/* Cache for further use */
			foreach ($results as $i) {
				Gene_Object::cacheObject($i['object_id'], $i);
			}
			
			$this->childs[$k] = $results;
			return $results;
		}
	}
	
	/**
	 * Lists all properties of the object.
	 *
	 * @return array Object Properties
	 */
	function &listProperties() {
		$db =& Gene::getDatabase();
		
		if ($this->properties === null) {
			$this->properties = $db->extended->getAssoc("SELECT property_id, value FROM ".Gene::tableName("object_properties")." WHERE object_id=?", null, array($this->objectID));
		}
		
		return $this->properties;
	}
	
	/**
	 * Gets a specific property of the object.
	 *
	 * @var string $property Property Name
	 *
	 * @return string Object Property Value
	 */
	function getProperty($property) {
		$properties =& $this->listProperties();
		
		if (isset($properties[$property])) {
			return $properties[$property];
		} else {
			return $this->getDefaultProperty($property);
		}
	}
	
	/**
	 * Gets the default value of a specific property.
	 *
	 * @var string $property Property Name
	 *
	 * @return string Object Property Default Value
	 */
	function getDefaultProperty($property) {
		if (isset($this->defaultProperties[$property])) {
			return $this->defaultProperties[$property];
		}
	}
	
	/**
	 * Sets a property of the object.
	 *
	 * @var string $property Property Name
	 * @var string $value Property Value
	 */
	function setProperty($property, $value=null) {
		$db =& Gene::getDatabase();
		$properties =& $this->listProperties();
		
		if (isset($properties[$property])) {
			if ($value === '' || $value === null) {
				$q = $db->prepare ("DELETE FROM ".Gene::tableName("object_properties")." WHERE object_id=? AND property_id=?");
				$q->execute(array($this->objectID, $property));
				unset($properties[$property]);
			} else {
				if ($value != $properties[$property]) {
					$db->extended->autoExecute(
						Gene::tableName("object_properties"),
						array(
							"value" => $value,
						),
						MDB2_AUTOQUERY_UPDATE,
						"object_id=".$db->quote($this->objectID)." AND property_id=".$db->quote($property)
					);
					
					$properties[$property] = $value;
				}
			}
		} else {
			$db->extended->autoExecute(
				Gene::tableName("object_properties"),
				array(
					"object_id" => $this->objectID,
					"property_id" => $property,
					"value" => $value,
				),
				MDB2_AUTOQUERY_INSERT
			);
			
			$properties[$property] = $value;
		}
		
		return true;
	}
	
	/**
	 * Lists all user's properties of the object.
	 *
	 * @return array Object User Properties
	 */
	function &listUserProperties() {
		$db =& Gene::getDatabase();
		$user =& Gene::getUser();
		
		if ($this->userProperties === null) {
			$this->userProperties = $db->extended->getAssoc("SELECT property_id, value FROM ".Gene::tableName("object_user_properties")." WHERE user_id=? AND object_id=?", null, array($user->getUserId(), $this->objectID));
		}
		
		return $this->userProperties;
	}
	
	/**
	 * Gets a specific user property of the object.
	 *
	 * @var string $property Property Name
	 *
	 * @return string Object UserProperty
	 */
	function getUserProperty($property) {
		$properties =& $this->listUserProperties();
		
		if (isset($properties[$property])) {
			return $properties[$property];
		} else {
			return $this->getDefaultUserProperty($property);
		}
	}
	
	/**
	 * Gets the default value of a specific user property.
	 *
	 * @var string $property Property Name
	 *
	 * @return string Object User Property Default Value
	 */
	function getDefaultUserProperty($property) {
		if (isset($this->defaultUserProperties[$property])) {
			return $this->defaultUserProperties[$property];
		}
	}
	
	/**
	 * Sets a specific user property of the object.
	 *
	 * @var string $property Property Name
	 * @var string $value Property Value
	 */
	function setUserProperty($property, $value=null) {
		$db =& Gene::getDatabase();
		$user =& Gene::getUser();
		$properties =& $this->listUserProperties();
		
		if (isset($properties[$property])) {
			if ($value == '') {
				$q = $db->prepare ("DELETE FROM ".Gene::tableName("object_user_properties")." WHERE object_id=? AND user_id=? AND property_id=?");
				$q->execute(array($this->objectID, $user->getUserId(), $property));
				unset($properties[$property]);
			} else {
				if ($value != $properties[$property]) {
					$db->extended->autoExecute(
						Gene::tableName("object_user_properties"),
						array(
							"value" => $value,
						),
						MDB2_AUTOQUERY_UPDATE,
						"object_id=".$this->objectID." AND user_id=".$user->getUserId()." AND property_id=".$db->quote($property)
					);
					
					$properties[$property] = $value;
				}
			}
		} else if ($value != null) {
			$db->extended->autoExecute(
				Gene::tableName("object_user_properties"),
				array(
					"object_id" => $this->objectID,
					"user_id" => $user->getUserId(),
					"property_id" => $property,
					"value" => $value,
				),
				MDB2_AUTOQUERY_INSERT
			);
			
			$properties[$property] = $value;
		}
		
		return true;
	}
	
	/**
	 * Returns previous revisions for this object ordered
	 * most recent first. If offset and limit are not
	 * specified, all revisions are returned.
	 *
	 * @var integer $offset Revision Offset
	 * @var integer $limit Number of Revisions
	 * @return array Previous Revisions
	 */
	function getRevisions($offset=null, $limit=null) {
		$db =& Gene::getDatabase();
		
		$offset = $offset ? intval($offset) : '0';
		$limit = $limit ? intval($limit) : '50';
		
		$db->setLimit($limit, $offset);
		
		$q = $db->prepare("SELECT r.*, u.name FROM ".Gene::tableName("object_revisions")." r LEFT JOIN ".Gene::tableName("user_info")." u ON (r.user_id=u.user_id) WHERE r.object_id=? ORDER BY r.rev_time DESC");
		
		$res = $q->execute(array($this->objectID));
		
		return $res->fetchAll();
	}
	
	/**
	 * Returns Revision ID specified for current object.
	 *
	 * @var integer $revisionid Revision ID
	 * @return array Revision Info
	 */
	function getRevision($revisionid) {
		$db =& Gene::getDatabase();

		$q = $db->prepare("SELECT r.*, u.name FROM ".Gene::tableName("object_revisions")." r LEFT JOIN ".Gene::tableName("user_info")." u ON (r.user_id=u.user_id) WHERE r.revision_id=? ORDER BY r.rev_time DESC");
		$res = $q->execute(array($revisionid));
		if ($revision = $res->fetchRow()) {
			$res->free();
			if ($revision['object_id'] == $this->objectID) {
				return $revision;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}
	
	/**
	 * Returns Gene_Object_Taxonomy class for
	 * object's taxonomy.
	 *
	 * @var integer $setId Taxonomy Set ID (Should be an Object ID)
	 * @return object Gene_Object_Taxonomy class
	 */
	function &getTaxonomyEntry($setId) {
		require_once ("Gene/Object/Taxonomy/Entry.php");
		$taxonomy =& new Gene_Object_Taxonomy_Entry($this->objectID, (int)$setId);
		return $taxonomy;
	}
	
	/**
	 * Returns this object's taxonomy set.
	 *
	 * @return object Gene_Object_Taxonomy_Set class
	 */
	function &getTaxonomySet() {
		require_once ("Gene/Object/Taxonomy/Set.php");
		$set =& new Gene_Object_Taxonomy_Set($this->objectID);
		return $set;
	}
	
	/**
	 * Returns Gene_Object_Taxonomy class for
	 * user's object taxonomy.
	 *
	 * @var integer $setId Taxonomy Set ID (Should be an Object ID)
	 * @var integer $userId User ID
	 *
	 * @return object Gene_Object_Taxonomy class
	 */
	function &getUserTaxonomyEntry($setId, $userId=null) {
		require_once ("Gene/Object/Taxonomy/Entry.php");
		
		if (!$userId) {
			$user =& Gene::getUser();
			$userId = $user->getUserId();
		}
		
		$taxonomy =& new Gene_Object_Taxonomy_Entry($this->objectID, (int)$setId, $userId);
		return $taxonomy;
	}
	
	/**
	 * Returns user's object taxonomy set.
	 *
	 * @var integer $userId User ID
	 *
	 * @return object Gene_Object_Taxonomy_Set class
	 */
	function &getUserTaxonomySet($userId=null) {
		require_once ("Gene/Object/Taxonomy/Set.php");
		
		if (!$userId) {
			$user =& Gene::getUser();
			$userId = $user->getUserId();
		}
		
		$set =& new Gene_Object_Taxonomy_Set($this->objectID, $userId);
		return $set;
	}
	 
	/**
	 * Returns object information.
	 *
	 * @return array Object Information
	 */
	function &get() {
		return $this->info;
	}
	
	/**
	 * Returns additional object information.
	 *
	 * @var string $tablename Table Name
	 *
	 * @return array Additional Object Information
	 */
	function getAdditionalInfo($tablename) {
		$db =& Gene::getDatabase();
		$q = $db->prepare("SELECT * FROM ".$tablename." WHERE object_id=?");
		$res = $q->execute(array($this->objectID));
		return $res->fetchRow();
	}
	
	/**
	 * Returns Gene_Object_View object for current object.
	 *
	 * @return object Gene_Object_View
	 */
	function &getView() {
		$info = $this->get();
		
		if (!isset($this->view)) {
			require_once ("Gene/Object/View/".ucfirst($info['type']).".php");
			$classname = "Gene_Object_View_".ucfirst($info['type']);
			$this->view =& new $classname($this);
		}
		
		return $this->view;
	}
}


 
?>